﻿/******************************************************************************************************
Copyright (c) 2014 Steven Giacomelli (stevegiacomelli@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************************************/

using System;
using System.IO;
using System.Text;

namespace SharpGeo.IO.ShapeFiles
{
    /// <summary>
    /// A static class for reading DBase files for use with the ShapeFile specification
    /// </summary>
    public static class DBaseFile
    {
        #region Methods

        #region Helper Functions

        private static int GetNullIndex(byte[] bytes, int index, int length)
        {
            for (int i = 0; i < length; i++)
                if (bytes[index + i] == 0x00)
                    return i;

            return length;
        }

        private static DBaseFieldType GetFieldType(byte b)
        {
            switch (b)
            {
                case 0x2B: // '+'
                    return DBaseFieldType.AutoIncrement;

                case 0x40: // '@'
                    return DBaseFieldType.Timestamp;

                case 0x42: //'B'
                    return DBaseFieldType.Binary;

                case 0x43: //'C'
                    return DBaseFieldType.Character;

                case 0x44: //'D'
                    return DBaseFieldType.Date;

                case 0x46: //'F'
                    return DBaseFieldType.Float;

                case 0x47: //'G'
                    return DBaseFieldType.OleData;

                case 0x49: //'I'
                    return DBaseFieldType.Long;

                case 0x4C: //'L'
                    return DBaseFieldType.Logical;

                case 0x4D: //'M'
                    return DBaseFieldType.Memo;

                case 0x4E: //'N'
                    return DBaseFieldType.Numeric;

                case 0x4F: //'O'
                    return DBaseFieldType.Double;

                default:
                    return DBaseFieldType.Unknown;
            }
        }

        #endregion Helper Functions

        #region Read Methods

        /// <summary>
        /// Reads a DBase file (up to DBase ver 5) and returns a DBaseGraph
        /// </summary>
        /// <param name="file">The file to read</param>
        /// <returns>The DBaseGraph or null if an error occurs</returns>
        public static DBaseGraph Read(string file)
        {
            if (!File.Exists(file))
                return null;

            using (var f = File.OpenRead(file))
            {
                return Read(f);
            }
        }

        /// <summary>
        /// Reads a DBase file (up to DBase ver 5) and returns a DBaseGraph
        /// </summary>
        /// <param name="stream">The stream to read the file from</param>
        /// <returns>The DBaseGraph or null if an error occurs</returns>
        public static DBaseGraph Read(Stream stream)
        {
            if (stream == null || !stream.CanRead)
            {
                return null;
            }

            var header = new byte[32];

            //try to read the header
            if (stream.Read(header, 0, 32) != 32)
                return null;

            var version = header[0] & 0x07;

            if (version != 3)
            {
                return null;
            }

            var graph = new DBaseGraph()
            {
                LastUpdated = new DateTime(header[1] + 1990, header[2], header[3]),
                NumberOfRows = ByteConverter.ToLittleEndianInt32(header, 4),
                LanguageId = header[29]
            };

            //read the field descriptors

            do
            {
                var fieldDescriptor = new byte[32];

                //try to read the Field descriptor
                if (stream.Read(fieldDescriptor, 0, 32) != 32)
                    break;

                graph.AddFieldDescriptor(new DBaseFieldDescriptorGraph()
                {
                    FieldName = ASCIIEncoding.ASCII.GetString(
                        fieldDescriptor, 0, GetNullIndex(fieldDescriptor, 0, 11)).Trim(),
                    FieldType = GetFieldType(fieldDescriptor[11]),
                    FieldLength = (int)fieldDescriptor[16],
                    DecimalCount = (int)fieldDescriptor[17]
                });

                //check for the terminator byte
                var terminator = stream.ReadByte();

                if (terminator == 0x0D)
                    break;
                else
                    stream.Seek(-1, SeekOrigin.Current);
            } while (true);

            //read the data

            for (int i = 0; i < graph.NumberOfRows; i++)
            {
                var rowStatus = stream.ReadByte();

                //if a row in the field is not valid then assume the file is not valid for use with the ShapeFile
                if (rowStatus != 0x20)
                    return null;

                for (int j = 0; j < graph.FieldDescriptors.Count; j++)
                {
                    byte[] rawData = new byte[graph.FieldDescriptors[j].FieldLength];

                    if (stream.Read(rawData, 0, rawData.Length) != rawData.Length)
                        return null;

                    graph.AddData(i, j, rawData);
                }
            }

            return graph;
        }

        #endregion Read Methods

        #region CSV Write Methods

        /// <summary>
        /// Writes the DBaseGraph to a CSV file
        /// </summary>
        /// <param name="fileName">The file to write to</param>
        /// <param name="graph">The graph to write</param>
        public static void WriteCsv(string fileName, DBaseGraph graph)
        {
            if (graph == null || !File.Exists(fileName))
                return;

            using (var f = File.OpenWrite(fileName))
            using (var w = new StreamWriter(f, Encoding.UTF8, 4096, true))
            {
                WriteCsv(w, graph);
            }
        }

        /// <summary>
        /// Writes a DBaseGraph to a CSV file
        /// </summary>
        /// <param name="textWriter">The TextWriter to write to</param>
        /// <param name="graph">The graph to write</param>
        public static void WriteCsv(TextWriter textWriter, DBaseGraph graph)
        {
            if (textWriter == null || graph == null)
                return;

            var header = new string[graph.FieldDescriptors.Count];
            for (int i = 0; i < graph.FieldDescriptors.Count; i++)
            {
                header[i] = graph.FieldDescriptors[i].FieldName;
            }

            textWriter.WriteLine(string.Join(",", header));

            foreach (var row in graph)
            {
                var rowString = new string[row.Length];

                for (int i = 0; i < row.Length; i++)
                {
                    if (row[i] == null)
                    {
                        rowString[i] = string.Empty;
                    }
                    else if (graph.FieldDescriptors[i].FieldType == DBaseFieldType.Character)
                    {
                        rowString[i] = "\"" + (string)row[i] + "\"";
                    }
                    else
                    {
                        rowString[i] = row[i].ToString();
                    }
                }

                textWriter.WriteLine(string.Join(",", rowString));
            }
        }

        #endregion CSV Write Methods

        #endregion Methods
    }
}